package com.netx.generics.basic;
import java.io.InputStream;
import java.util.Map;
import java.util.HashMap;
import java.util.List;
import java.util.ArrayList;
import java.util.Iterator;
import com.netx.generics.translation.Results;
import com.netx.generics.translation.Translator;
import com.netx.generics.collections.ImmutableList;
import com.netx.generics.io.BufferedReader;


public class Context {

	// TYPE:
	private static final RootContext _root = new RootContext();
	
	static {
		// Dispose contexts:
		Runtime.getRuntime().addShutdownHook(new Thread() {
			public void run() {
				_dispose(getRootContext());
			}
		});
	}

	public static RootContext getRootContext() {
		return _root;
	}

	public static Logger getRootLogger() {
		return getRootContext().getLogger();
	}

	public static Results loadFrom(InputStream in, Map<String,ContextRules> rules) {
		Checker.checkNull(in, "in");
		final ContextScanner scanner = new ContextScanner(new BufferedReader(in));
		final ContextParser parser = new ContextParser(scanner);
		final ContextAnalyzer analyzer = new ContextAnalyzer(parser);
		if(rules != null) {
			ContextRulesApplyer applyer = new ContextRulesApplyer(analyzer, rules);
			// fool the compiler:
			applyer.toString();
		}
		return new Translator(scanner, Translator.TRANSLATION_POLICY.STOP_ON_NULL).performWork();
	}
	
	public static Results loadFrom(InputStream in) {
		return loadFrom(in, null);
	}

	private static void _dispose(Context ctx) {
		ctx.onDispose();
		Iterator<Context> it = ctx.getContexts().iterator();
		while(it.hasNext()) {
			_dispose(it.next());
		}
		ctx._contexts.clear();
		ctx._properties.clear();
		ctx._listeners.clear();
	}

	// INSTANCE:
	private final String _name;
	private final Context _parent;
	private final Map<String,Property> _properties;
	private final Map<String,Context> _contexts;
	private final List<PropertySetListener> _listeners;
	private ContextRules _rules;

	public Context(String name, Context parent) {
		Checker.checkTextIdentifier(name, "name", true);
		_name = name;
		if(getClass() == RootContext.class) {
			_parent = null;
		}
		else {
			Checker.checkNull(parent, "parent");
			_parent = parent;
			_parent._setContext(this);
		}
		_properties = new HashMap<String,Property>();
		_contexts = new HashMap<String,Context>();
		_listeners = new ArrayList<PropertySetListener>();
		_rules = new ContextRules(true);
		_listeners.add(new RulesApplyerListener());
	}

	public String getName() {
		return _name;
	}
	
	public Context getParent() {
		return _parent;
	}
	
	public String getPath() {
		StringBuilder path = new StringBuilder();
		path.append(getName());
		Context parent = getParent();
		while(parent != null) {
			path.insert(0, parent.getName()+"/");
			parent = parent.getParent();
		}
		return path.toString();
	}

	public Context getContext(String path) {
		ContextPath cpath = _breakContextPath(path);
		return _getContext(cpath);
	}
	
	public Property getProperty(String path) {
		ContextPath cpath = _breakPropertyPath(path);
		if(cpath.path == null) {
			return _properties.get(cpath.name);
		}
		else {
			Context c = _getContext(cpath);
			if(c == null) {
				return null;
			}
			else {
				return c._properties.get(cpath.name);
			}
		}
	}
	
	public Context setProperty(String path, Object value) {
		ContextPath cpath = _breakPropertyPath(path);
		// If this is the target Context, set the property for self:
		if(cpath.path == null) {
			Property p = new Property(cpath.name, value);
			// Run listeners.onBeforeSet:
			Iterator<PropertySetListener> it = _listeners.iterator();
			while(it.hasNext()) {
				it.next().onBeforeSet(p, this);
			}
			// Set the property:
			_properties.put(p.getName(), p);
			// Run listeners.onAfterSet:
			it = _listeners.iterator();
			while(it.hasNext()) {
				it.next().onAfterSet(p, this);
			}
			return this;
		}
		// Otherwise, request the owner Context to set it:
		else {
			Context owner = _getContext(cpath);
			if(owner == null) {
				throw new ObjectNotFoundException("context '"+cpath.failPath+"'");
			}
			else {
				owner.setProperty(cpath.name, value);
				return owner;
			}
		}
	}
	
	public Context setProperty(Property p) {
		Checker.checkNull(p, "p");
		return setProperty(p.getName(), p.objectValue());
	}

	public Context removeContext(String path) {
		ContextPath cpath = _breakContextPath(path);
		Context c = this;
		for(int i=0; i<cpath.path.length-1; i++) {
			c = c._contexts.get(cpath.path[i]);
			if(c == null) {
				return null;
			}
		}
		return c._contexts.remove(cpath.path[cpath.path.length-1]);
	}
	
	public Property removeProperty(String path) {
		ContextPath cpath = _breakPropertyPath(path);
		Context owner = null;
		if(cpath.path == null) {
			owner = this;
		}
		else {
			owner = _getContext(cpath);
		}
		if(owner == null) {
			return null;
		}
		else {
			return owner._properties.remove(cpath.name);
		}
	}

	public void onDispose() {
		// noop;
	}

	public ImmutableList<Context> getContexts() {
		return new ImmutableList<Context>(new ArrayList<Context>(_contexts.values()));
	}

	public ImmutableList<Property> getProperties() {
		return new ImmutableList<Property>(new ArrayList<Property>(_properties.values()));
	}
	
	public void addPropertySetListener(PropertySetListener listener) {
		Checker.checkNull(listener, "listener");
		_listeners.add(listener);
	}
	
	public void applyRules(ContextRules rules, boolean overwriteDefs) throws RulesException {
		Checker.checkNull(rules, "rules");
		List<String> errors = new ArrayList<String>();
		// Import the new rules:
		_rules.setAllowOtherProperties(rules.getAllowOtherProperties());
		Iterator<PropertyDefinition> itDefs = rules.getDefinitions().values().iterator();
		while(itDefs.hasNext()) {
			PropertyDefinition def = itDefs.next();
			if(overwriteDefs==false && _rules.getDefinitionFor(def.getPropertyName()) != null) {
				errors.add("property definition for property '"+def.getPropertyName()+"' already exists");
			}
			else {
				_rules.add(def);
			}
		}
		// Apply all the defs:
		itDefs = _rules.getDefinitions().values().iterator();
		while(itDefs.hasNext()) {
			PropertyDefinition def = itDefs.next();
			Property p = getProperty(def.getPropertyName());
			if(p == null) {
				if(def.isMandatory()) {
					errors.add("mandatory property '"+def.getPropertyName()+"' missing");
				}
				else {
					setProperty(def.getPropertyName(), def.getDefaultValue());
				}
			}
			else {
				try {
					def.validate(p.objectValue(), null);
				}
				catch(ValidationException ve) {
					errors.add("in '"+p.getName()+"': "+ve.getMessage());
					removeProperty(p.getName());
				}
			}
		}
		// Check whether there are properties not specified in defs:
		if(_rules.getAllowOtherProperties() == false) {
			Iterator<Property> itProps = _properties.values().iterator();
			while(itProps.hasNext()) {
				Property p = itProps.next();
				if(_rules.getDefinitionFor(p.getName()) == null) {
					errors.add("illegal property '"+p.getName()+"'");
					itProps.remove();
				}
			}
		}
		// Finalize:
		if(!errors.isEmpty()) {
			throw new RulesException(new ErrorList("errors applying ContextRules", errors));
		}
	}
	
	public void applyRules(ContextRules rules) throws RulesException {
		applyRules(rules, true);
	}

	public String toString() {
		return _name;
	}

	public void finalize() {
		try {
			_properties.clear();
			_contexts.clear();
			super.finalize();
		}
		catch(Throwable t) {
			getRootContext().getLogger().error(t);
		}
	}
	
	// returns the path in ContextPath.path. name is always null.
	// if path is a direct name, ContextPath.path.length == 1.
	private ContextPath _breakContextPath(String path) {
		if(path.startsWith("/")) {
			throw new IllegalArgumentException("context path cannot start with '/'");
		}
		Checker.checkTextIdentifier(path.replaceAll("/", ""), "path", true);
		// just name:
		if(!path.contains("/")) {
			return new ContextPath(new String[] {path}, null);
		}
		// composed path:
		else {
			return new ContextPath(path.split("[/]"), null);
		}
	}

	// returns the name of the property, and a context path only if
	// the path is composed (otherwise, ContextPath.path is null)
	private ContextPath _breakPropertyPath(String path) {
		Checker.checkEmpty(path, "path");
		// Composed path:
		if(path.contains("/")) {
			int index = path.lastIndexOf("/");
			String name = path.substring(index+1);
			// treat regular context path:
			path = path.substring(0, index);
			ContextPath cpath = _breakContextPath(path);
			return new ContextPath(cpath.path, name);
		}
		// Just the property name:
		else {
			Checker.checkTextIdentifier(path, "path", true);
			return new ContextPath(null, path);
		}
	}
	
	private Context _getContext(ContextPath cpath) {
		Context c = this;
		// Used to create the path until which the context is found:
		StringBuilder fp = new StringBuilder();
		for(int i=0; i<cpath.path.length; i++) {
			c = c._contexts.get(cpath.path[i]);
			if(c == null) {
				fp.append(cpath.path[i]);
				cpath.failPath = fp.toString();
				return null;
			}
			else {
				fp.append(cpath.path[i]);
				fp.append("/");
			}
		}
		return c;
	}

	private class ContextPath {
		
		public final String name;
		public final String[] path;
		public String failPath;
		
		public ContextPath(String[] path, String name) {
			this.path = path;
			this.name = name;
			this.failPath = null;
		}
	}

	private void _setContext(Context child) {
		if(_contexts.get(child.getName()) != null) {
			throw new ObjectAlreadyExistsException(child.getName());
		}
		else {
			_contexts.put(child.getName(), child);
		}
	}
	
	private class RulesApplyerListener implements PropertySetListener {
		public RulesApplyerListener() {
		}
		
		public void onBeforeSet(Property p, Context c) {
			PropertyDefinition pd = _rules.getDefinitionFor(p.getName());
			if(pd != null) {
				// Get old property value, if any:
				Property p2 = c.getProperty(p.getName());
				// Validate; if there are problems, a ValidationException will be thrown.
				pd.validate(p.objectValue(), p2);
			}
			else {
				if(c._rules.getAllowOtherProperties() == false) {
					throw new IllegalArgumentException("property '"+p.getName()+"' is not allowed");
				}
			}
		}
		
		public void onAfterSet(Property p, Context c) {
			// noop;
		}
	}
}
